package clock;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Observable;
import java.util.Observer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;

@SuppressWarnings("serial")
public class Clock extends JFrame
{
	public static void main(String argv[])
	{
		new Clock("variable-speed digital clock");
	}

	private SecondsCounter counter;
	private ClockDisplay display;
	private ControlPanel controlPanel;

	private final double initialSpeed = 1.0;
	private final long initialTime = 8 * 60 * 60; // 8 am in seconds
	private final boolean initialRunning = false;
	private final boolean initialDisplaySeconds = true;
	private final boolean initialTwelveHour = false;



	public Clock(String title) { // constructor
		super(title);
		setSize(600, 400);
		setVisible(true);

		counter = new SecondsCounter(initialSpeed, initialTime, initialRunning);

		display = new ClockDisplay(counter, initialDisplaySeconds,
				initialTwelveHour);
		getContentPane().add("Center", display);

		controlPanel = new ControlPanel(initialTime, initialSpeed);
		getContentPane().add("South", controlPanel);

		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	}

	private class SecondsCounter extends Observable
	{

		private AtomicLong seconds = new AtomicLong(); // the count
		private AtomicBoolean running = new AtomicBoolean();
		private Ticker ticker;



		public SecondsCounter(double speed, long secs, boolean run) { // constructor
			seconds.set(secs);
			running.set(run);
			ticker = new Ticker(this, speed);
		}



		public void set(long s)
		{
			seconds.set(s);
			setChanged();
			notifyObservers();
		}



		public void tick()
		{
			seconds.getAndIncrement();
			setChanged();
			notifyObservers();
		}



		public long get()
		{
			return seconds.get();
		}



		public void setRunning(boolean r)
		{
			running.set(r);
		}



		public boolean getRunning()
		{
			return running.get();
		}



		public void setSpeed(double spd)
		{
			ticker.resetSpeed(spd);
		}
	}

	private class Ticker
	{
		private SecondsCounter counter;
		private java.util.Timer timer;



		public Ticker(SecondsCounter c, double speed) {
			counter = c;
			timer = new java.util.Timer();
			timer.scheduleAtFixedRate(new TickerTask(), 0, // initial delay
					Math.round(1000 / speed)); // interval
		}



		public void resetSpeed(double speed)
		{
			timer.cancel();
			timer = new java.util.Timer();
			timer.scheduleAtFixedRate(new TickerTask(), 0,
					Math.round(1000 / speed));
		}

		private class TickerTask extends TimerTask
		{
			public void run()
			{
				if (counter.getRunning())
					counter.tick();
			}
		}
	}

	private class ClockDisplay extends JComponent
	{
		private SecondsCounter counter;
		private TimeFormatter formatter;



		public ClockDisplay(SecondsCounter c, // constructor
				boolean displaySeconds, boolean twelveHour) {
			counter = c;
			counter.addObserver(new ClockWatcher(this));
			formatter = new TimeFormatter();
			formatter.setDisplaySeconds(displaySeconds);
			formatter.setTwelveHour(twelveHour);
		}



		public void paint(Graphics g)
		{
			// erase old contents:
			Dimension dim = getSize();
			g.setColor(Color.white);
			g.fillRect(0, 0, dim.width, dim.height);

			g.setColor(Color.black);
			g.setFont(new Font("Helvetica", Font.PLAIN, 80));

			int c = (int) (counter.get() % (long) (24 * 60 * 60));
			String time = formatter.convert(c);
			drawStringCentered(g, time);
		}



		public void setDisplaySeconds(boolean b)
		{
			formatter.setDisplaySeconds(b);
			repaint();
		}



		public void setTwelveHour(boolean b)
		{
			formatter.setTwelveHour(b);
			repaint();
		}



		private void drawStringCentered(Graphics g, String s)
		{

			Dimension d = getSize();
			FontMetrics f = g.getFontMetrics();
			int stringWidth = f.stringWidth(s);
			int stringHeight = f.getAscent();
			int xOffset = (d.width - stringWidth) / 2;
			int yOffset = (d.height + stringHeight) / 2; // of baseline
			g.drawString(s, xOffset, yOffset);
		}
	}

	private class ClockWatcher implements Observer
	{

		private ClockDisplay display;



		public ClockWatcher(ClockDisplay d) { // constructor
			display = d;
		}



		public void update(Observable obs, Object obj)
		{
			display.repaint();
		}
	}

	private class TimeFormatter
	{

		AtomicBoolean displaySeconds = new AtomicBoolean();
		AtomicBoolean twelveHour = new AtomicBoolean();



		public void setDisplaySeconds(boolean b)
		{
			displaySeconds.set(b);
		}



		public void setTwelveHour(boolean b)
		{
			twelveHour.set(b);
		}



		public String convert(int t)
		{
			boolean twelveHr = twelveHour.get();
			// get once to use twice: avoid
			// having it set beween uses!
			int h = t / 3600; // hours
			if (twelveHr) {
				if (h == 0) // midnight
					h = 12;
				else if (h > 12) // hour is 1 PM or later
					h = h - 12;
			}

			int m = (t % 3600) / 60; // minutes

			String time = String.format("%2d:%02d", h, m);
			if (displaySeconds.get()) {
				int s = t % 60; // seconds
				time += String.format(":%02d", s);
			}

			if (twelveHr) {
				if (t < 12 * 60 * 60) // note: "=" is 12 PM
					time += " am";
				else
					time += " pm";
			}
			return time;
		}

	}

	private class ButtonGroup extends JPanel
	{
		private Listener listener = new Listener();
		private JButton runningTrue, runningFalse, // invoke counter.setRunning
				secondsTrue, secondsFalse, // invoke display.setDisplaySeconds
				twelveTrue, twelveFalse; // invoke display.setTwelveHour



		public ButtonGroup() { // constructor
			setLayout(new GridLayout(3, 2, 5, 10)); // 3 rows, 2 columns,
													// column spacing 5,
													// row spacing 10
			runningTrue = addButton("run");
			runningFalse = addButton("pause");
			secondsTrue = addButton("show seconds");
			secondsFalse = addButton("don't show seconds");
			twelveTrue = addButton("12-hour clock");
			twelveFalse = addButton("24-hour clock");
		}



		private JButton addButton(String label)
		{
			JButton button = new JButton(label);
			add(button); // to grid
			button.addActionListener(listener);
			return button;
		}

		private class Listener implements ActionListener
		{
			public void actionPerformed(ActionEvent e)
			{
				if (e.getSource() == runningTrue)
					counter.setRunning(true);
				else if (e.getSource() == runningFalse)
					counter.setRunning(false);
				else if (e.getSource() == secondsTrue)
					display.setDisplaySeconds(true);
				else if (e.getSource() == secondsFalse)
					display.setDisplaySeconds(false);
				else if (e.getSource() == twelveTrue)
					display.setTwelveHour(true);
				else if (e.getSource() == twelveFalse)
					display.setTwelveHour(false);
			}
		}
	}

	private class SpeedControl extends JPanel
	{
		private Listener listener = new Listener();
		private JTextField field;



		public SpeedControl(double initialSpeed) { // constructor
			add(new JLabel("set speed:"), "West");
			String initial = String.format("%3.1f", initialSpeed);
			field = new JTextField(initial, 5);
			add(field, "East");
			field.addActionListener(listener);
		}

		private class Listener implements ActionListener
		{
			public void actionPerformed(ActionEvent e)
			{
				try {
					Double newSpeed = Double
							.parseDouble(field.getText().trim());
					if (1e-9 < newSpeed && newSpeed < 2000.0)
						counter.setSpeed(newSpeed);
					else
						blink();
				}
				catch (NumberFormatException nfe) {
					blink();
				}
			}
		}



		private void blink()
		{
			Graphics g = getGraphics();
			for (int i = 0; i < 3; i++) {
				field.setVisible(false);
				update(g);
				try {
					Thread.sleep(300);
				}
				catch (InterruptedException e) {
				}
				field.setVisible(true);
				update(g);
				try {
					Thread.sleep(300);
				}
				catch (InterruptedException e) {
				}
			}
		}
	}

	private class TimeResetter extends JPanel
	{
		private Listener listener = new Listener();
		private JTextField hour, minute, second;



		public TimeResetter(long initialTime) { // constructor
			setLayout(new GridLayout(1, 5, 0, 0)); // 1 row, 5 "columns"

			String initH = String.format("%2d", initialTime / 3600);
			hour = new JTextField(initH, 2);
			add(hour);

			add(new JLabel(":", JLabel.CENTER));
			String initM = String.format("%02d", (initialTime % 3600) / 60);
			minute = new JTextField(initM, 2);
			add(minute);

			String initS = String.format("%02d", initialTime % 60);
			add(new JLabel(":", JLabel.CENTER));
			second = new JTextField(initS, 2);
			add(second);

			hour.addActionListener(listener);
			minute.addActionListener(listener);
			second.addActionListener(listener);
		}

		private class Listener implements ActionListener
		{
			public void actionPerformed(ActionEvent e)
			{
				int h, m, s;
				try {
					h = Integer.parseInt(hour.getText().trim());
				}
				catch (NumberFormatException nfe) {
					blink(hour);
					return;
				}
				if (h < 0 || h > 23) {
					blink(hour);
					return;
				}

				try {
					m = Integer.parseInt(minute.getText().trim());
				}
				catch (NumberFormatException nfe) {
					blink(minute);
					return;
				}
				if (m < 0 || m > 59) {
					blink(minute);
					return;
				}

				try {
					s = Integer.parseInt(second.getText().trim());
				}
				catch (NumberFormatException nfe) {
					blink(second);
					return;
				}
				if (s < 0 || s > 59) {
					blink(second);
					return;
				}

				counter.set(h * 3600 + m * 60 + s);
			}
		}



		private void blink(JTextField f)
		{
			Graphics g = getGraphics();
			for (int i = 0; i < 3; i++) {
				f.setVisible(false);
				update(g);
				try {
					Thread.sleep(300);
				}
				catch (InterruptedException e) {
				}
				f.setVisible(true);
				update(g);
				try {
					Thread.sleep(300);
				}
				catch (InterruptedException e) {
				}
			}
		}
	}

	private class NumericWidgetPanel extends JPanel
	{

		public NumericWidgetPanel(long initialTime, // constructor
				double initialSpeed) {
			setLayout(new GridLayout(3, 1)); // 3 rows, 1 column
			add(new JLabel("set time (in 24-hour form):"));
			add(new TimeResetter(initialTime));
			add(new SpeedControl(initialSpeed));
		}
	}

	private class ControlPanel extends JPanel
	{

		public ControlPanel(long initialTime, // constructor
				double initialSpeed) {
			add("West", new NumericWidgetPanel(initialTime, initialSpeed));
			add("Center", new JLabel("        ")); // spacer
			add("East", new ButtonGroup());
		}
	}

}
